#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Time      :2021/7/2 14:44
# @Author    :cjw
import os
from time import sleep
from typing import Optional, Tuple

from appium.webdriver import WebElement as MobileWebElement
from appium.webdriver.common.mobileby import MobileBy
from appium.webdriver.webdriver import WebDriver
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from app_wework_po.util.log import logger
from app_wework_po.util.handle_uiselector import handle_result
from app_wework_po.setting import MOBILE_BY_RULES, WEBDRIVER_WAIT_TIMEOUT, IMPLICITLY_WAIT
from app_wework_po.util.appium_device_sync import AppiumDeviceSync


class BasePage:
	"""页面基类"""

	locators = {}  # 元素定位器

	def __init__(self, base_driver: Optional[WebDriver] = None):
		"""
		初始化页面
		:param base_driver: 起始app对象
		"""
		self.driver = base_driver

	def __getattr__(self, name: str):
		"""
		当使用【实例.属性】获取属性值时，如果实例没有该属性，就会调用这个方法
		用于实例属性访问，获取locators元素定位器中的值。
		locators元素定位的值保存在yml文件中,以字典的形式保存，
		字典的键是定位元素的名称，值是定位表达式【查找方式，查找值，flag标志存在即为查找多个】

		yml文件中保存的格式：
		department_name: # 部门名称
		  - xpath
		  - //ul[@role="group"]//a
		  - 1 # 这个值判断是否查找多个元素
		:param name: 定位元素的名称
		:return:
		"""
		# 判断定位元素是否存在
		if name not in self.locators:
			logger.error(msg := f'元素定位器locators中无定位属性的键：{name}')
			raise ValueError(msg)

		# 判断定位方式是否正确
		by, val, *flag = self.locators[name]
		if by not in MOBILE_BY_RULES:
			logger.error(msg := f'不支持该元素定位方式：{by}，定位内容：{val}')
			raise ValueError(msg)

		# 报错信息
		msg = f'查找元素超时，元素定位方式：{by}, 定位内容：{val}'

		# 对android uiautomator 定位值进行特殊处理
		if by == MobileBy.ANDROID_UIAUTOMATOR:
			val = handle_result(val)

		try:
			if flag:  # flag变量如果存在，表示需要查找多个元素
				element = WebDriverWait(self.driver, WEBDRIVER_WAIT_TIMEOUT).until(
					lambda x: x.find_elements(by, val), msg)
			else:
				element = WebDriverWait(self.driver, WEBDRIVER_WAIT_TIMEOUT).until(
					lambda x: x.find_element(by, val), msg)
			return element
		except TimeoutException as ex:
			logger.error(msg)
			raise ex

	def find(self, loc: Tuple[str, str], is_displayed: bool = True, is_enabled: bool = False,
	         timeout: int = WEBDRIVER_WAIT_TIMEOUT) -> MobileWebElement:
		"""
		查找单个元素，默认查找可见的元素
		:param loc: 定位元素：(定位方式，定位值)
		:param is_displayed: 元素是否可见
		:param is_enabled: 元素是否可操作
		:param timeout: 元素查找，超时时间
		:return:
		"""
		if loc[0] not in MOBILE_BY_RULES:
			logger.error(msg := f'不支持该元素定位方式：{loc[0]}，定位内容：{loc[1]}')
			raise ValueError(msg)

		# 报错信息
		msg = f'查找可见元素超时，元素定位方式：{loc[0]}, 定位内容：{loc[1]}'

		try:
			if is_displayed:
				element = WebDriverWait(self.driver, timeout).until(
					EC.visibility_of_element_located(loc), msg)
			elif is_enabled:
				element = WebDriverWait(self.driver, timeout).until(
					EC.element_to_be_clickable(loc), msg)
			else:
				element = WebDriverWait(self.driver, timeout).until(
					lambda x: x.find_element(*loc), msg)
			return element
		except TimeoutException as ex:
			logger.error(msg)
			raise ex

	def finds(self, loc: Tuple[str, str], is_displayed: bool = True,
	          timeout: int = WEBDRIVER_WAIT_TIMEOUT) -> MobileWebElement:
		"""
		查找多个元素, 默认查找可见的元素
		:param loc: 定位元素：(定位方式，定位值)
		:param is_displayed: 元素是否可见
		:param timeout: 元素查找，超时时间
		:return:
		"""
		if loc[0] not in MOBILE_BY_RULES:
			logger.error(f'不支持该元素定位方式：{loc[0]}，定位内容：{loc[1]}')

		# 报错信息
		msg = f'查找可见元素超时，元素定位方式：{loc[0]}, 定位内容：{loc[1]}'

		try:
			if is_displayed:
				element = WebDriverWait(self.driver, timeout).until(
					EC.visibility_of_all_elements_located(loc), msg)
			else:
				element = WebDriverWait(self.driver, timeout).until(
					lambda x: x.find_elements(*loc), msg)
			return element
		except TimeoutException as ex:
			logger.error(msg)
			raise ex

	def swipe(self, direction: str = 'up', num: int = 1, is_adb: bool = False,
	          window_size: Optional[Tuple[int, int]] = None, duration: int = 3000) -> None:
		"""
		屏幕滑动
		:param direction: 滑动方向：up-上，down-下，left-左，right-右
		:param num: 滑动次数，默认1次
		:param is_adb: 是否通过adb命令滑动
		:param window_size: 屏幕尺寸,(宽度，高度)
		:param duration: 持续时间，默认3000(ms)
		:return:
		"""
		# 滑动方向校验
		direction = direction.lower()
		directions = {'up': '上', 'down': '下', 'left': '左', 'right': '右'}  # 滑动方向
		if direction not in directions:  # 判断滑动方向是否存在
			logger.info(msg := f'滑动方向未定义: {direction}, 可选择的滑动方向：{directions}')
			raise ValueError(msg)

		# 判断是否需要获取屏幕大小数据
		if not window_size:
			size = self.driver.get_window_size()
			w, h = size['width'], size['height']
		else:
			w, h = window_size

		# 初始化滑动坐标
		start_x, start_y, end_x, end_y = 0, 0, 0, 0

		while True:  # 滑动
			try:
				logger.info(f'开始向{directions[direction]}滑动')
				if direction == 'up':  # 上滑
					start_x, start_y = 0.5 * w, 0.8 * h
					end_x, end_y = start_x, 0.2 * h
				elif direction == 'down':  # 下滑
					start_x, start_y = 0.5 * w, 0.2 * h
					end_x, end_y = start_x, 0.8 * h
				elif direction == 'left':  # 左滑
					start_x, start_y = 0.8 * w, 0.5 * h
					end_x, end_y = 0.2 * w, start_y
				elif direction == 'right':  # 右滑
					start_x, start_y = 0.2 * w, 0.5 * h
					end_x, end_y = 0.8 * w, start_y

				# 处理后的坐标进行取整
				start_x, start_y = int(start_x), int(start_y)
				end_x, end_y = int(end_x), int(end_y)

				if is_adb:  # 判断执行滑动方式
					# 获取当前driver的udid
					udid = self.driver.desired_capabilities.get('udid', None)
					if udid:
						# adb 滑动命令, android版本不低于4.4，可以模拟长按事件
						command = f'adb -P 5037 -s {udid} shell input swipe ' \
						          f'{start_x} {start_y} {end_x} {end_y} {duration}'
					else:
						# 设备数量判断
						if len(devices := AppiumDeviceSync.devices()) >= 2:
							logger.error(msg := f'当前连接的手机设备数量大于1，不能执行adb滑动，在线设备：{devices}')
							raise ValueError(msg)
						else:
							logger.info(f'启动参数未设定udid，多设备情况下不能执行adb滑动操作')
							command = f'adb -P 5037 shell input swipe ' \
							          f'{start_x} {start_y} {end_x} {end_y} {duration}'

					os.popen(command)  # 执行命令
					# 等待adb滑动持续时间，避免多次滑动重合进行
					sleep(round(duration / 1000))
				else:
					self.driver.swipe(start_x, start_y, end_x, end_y, duration)
				sleep(1)  # 滑动后，间隔1秒进行下一次滑动
			except Exception as e:
				logger.error(f'向{directions[direction]}滑动发送异常：异常信息：{e}')
				raise e

			num -= 1  # 每次滑动，滑动次数减1
			if not num:
				break

	def swipe_find(self, loc: Tuple[str, str], direction: str = 'up', num: int = 5,
	               is_adb: bool = False, duration: int = 5000) -> MobileWebElement:
		"""
		滑动查找元素：
		:param loc: 元素定位(定位方式，定位值)
		:param direction: 滑动方向
		:param num: 查找次数
		:param is_adb: 是否通过adb命令滑动
		:param duration: 持续时间，默认5000(ms)
		:return:
		"""
		# 判断定位方式是否正确
		if loc[0] not in MOBILE_BY_RULES:
			logger.error(msg := f'滑动查找，不支持该元素定位方式：{loc[0]}，定位内容：{loc[1]}')
			raise ValueError(msg)

		# 进入滑动查找，改变隐式等待时长，提高查找速度
		self.driver.implicitly_wait(1)

		# 获取当前屏幕大小数据
		size = self.driver.get_window_size()
		w, h = size['width'], size['height']

		for i in range(num):
			try:
				# 找到元素后，修改隐式等待时间，然后返回找到的元素对象
				ele = self.driver.find_element(*loc)
				self.driver.implicitly_wait(IMPLICITLY_WAIT)
				return ele
			except NoSuchElementException as ex:
				self.swipe(direction, is_adb=is_adb, duration=duration, window_size=(w, h))
				logger.info(f'第{i + 1}次滑动, 未找到定位元素：{loc}')

			if i == num - 1:  # 规定的滑动次数后，未找到元素，抛出异常
				self.driver.implicitly_wait(IMPLICITLY_WAIT)
				logger.error(msg := f'滑动查找{num}次，没有找到需要定位的元素：{loc}')
				raise NoSuchElementException(msg)

	def tap(self, ele: MobileWebElement) -> None:
		"""
		模拟手指点击, 点击元素的中心点，防止点偏
		:param ele: 要点击的元素对象
		:return:
		"""
		# 获取元素中心点坐标
		loc = ele.location  # 左上角坐标
		size = ele.size  # 元素的尺寸，宽和高
		tap_x = loc['x'] + 0.5 * size['width']
		tap_y = loc['y'] + 0.5 * size['height']

		try:
			# 不传入点击的duration(持续时间)，否则会出现点不进去的情况
			self.driver.tap([(int(tap_x), int(tap_y))])
		except Exception as ex:
			logger.error(f'点击元素失败，报错信息：{ex}')
			raise ex

	def find_click(self, loc: Tuple[str, str], is_displayed: bool = True, is_enabled: bool = False,
	               timeout: int = WEBDRIVER_WAIT_TIMEOUT) -> None:
		"""
		找到元素后点击
		:param loc: 定位元素：(定位方式，定位值)
		:param is_displayed: 元素是否可见
		:param is_enabled: 元素是否可操作
		:param timeout: 元素查找，超时时间
		:return:
		"""
		ele = self.find(loc, is_displayed, is_enabled, timeout)
		self.tap(ele)

	def swipe_find_click(self, loc: Tuple[str, str], direction: str = 'up', num: int = 5,
	                     is_adb: bool = False, duration: int = 5000) -> None:
		"""
		滑动查找点击
		:param loc: 元素定位(定位方式，定位值)
		:param direction: 滑动方向
		:param num: 查找次数
		:param is_adb: 是否通过adb命令滑动
		:param duration: 持续时间，默认5000(ms)
		:return:
		"""
		ele = self.swipe_find(loc, direction, num, is_adb, duration)
		self.tap(ele)

	def send_keys(self, ele: MobileWebElement, text: str) -> None:
		"""
		文本输入
		:param ele: 要输入的元素对象
		:param text: 输入值
		:return:
		"""
		self.tap(ele)  # 点击输入框
		ele.clear()  # 清空输入框
		try:
			ele.send_keys(text)  # 文本输入
		except Exception as ex:
			logger.info(f'输入文本失败，输入内容：{text}, 报错信息：{ex}')
			raise ex

	def find_send_keys(self, loc: Tuple[str, str], text: str, is_displayed: bool = True, is_enabled: bool = False,
	                   timeout: int = WEBDRIVER_WAIT_TIMEOUT) -> None:
		"""
		找到元素后点击
		:param loc: 定位元素：(定位方式，定位值)
		:param text: 输入的文本
		:param is_displayed: 元素是否可见
		:param is_enabled: 元素是否可操作
		:param timeout: 元素查找，超时时间
		:return:
		"""
		ele = self.find(loc, is_displayed, is_enabled, timeout)
		self.send_keys(ele, text)

	def swipe_send_keys(self, loc: Tuple[str, str], text: str, direction: str = 'up', num: int = 5,
	                    is_adb: bool = False, duration: int = 5000) -> None:
		"""
		滑动查找点击
		:param loc: 元素定位(定位方式，定位值)
		:param text: 输入的文本
		:param direction: 滑动方向
		:param num: 查找次数
		:param is_adb: 是否通过adb命令滑动
		:param duration: 持续时间，默认5000(ms)
		:return:
		"""
		ele = self.swipe_find(loc, direction, num, is_adb, duration)
		self.send_keys(ele, text)

	@property
	def toast_text(self) -> str:
		"""获取toast系统提示信息"""
		tip_text = self.find((MobileBy.XPATH, '//*[@class="android.widget.Toast"]'),
		                     is_displayed=False).text
		return tip_text
